﻿using Microsoft.Web.WebView2.Core;
using System;
using System.Collections.Generic;
using System.Drawing.Imaging;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;

namespace WebCapture
{
    /// <summary>
    /// 提供基于 WebView2 的网页全页截图功能，支持单次截图与滚动拼接两种模式。
    /// 该类完全解耦 UI，通过事件通知进度、日志和结果，适用于 WinForms/WPF 等桌面应用。
    /// </summary>
    public class WebCaptureHelper : IDisposable
    {
        // ===== 事件定义：用于向 UI 层传递状态信息 =====

        /// <summary>
        /// 日志输出事件。UI 可订阅此事件以显示操作日志（如“开始截图”、“保存成功”等）。
        /// </summary>
        public event Action<string> OnLog = delegate { };

        /// <summary>
        /// 进度更新事件。参数为百分比 (0-100) 和可选状态描述。
        /// </summary>
        public event Action<int, string> OnProgress = delegate { };

        /// <summary>
        /// 截图完成事件。参数为最终保存的文件路径。
        /// </summary>
        public event Action<string> OnCaptureComplete = delegate { };

        /// <summary>
        /// 截图开始事件。可用于启用“取消”按钮或禁用其他操作。
        /// </summary>
        public event Action OnCaptureStart = delegate { };

        /// <summary>
        /// 用户取消截图事件。可用于恢复 UI 状态。
        /// </summary>
        public event Action OnCaptureCancelled = delegate { };

        // ===== 常量与私有字段 =====

        /// <summary>
        /// 单次截图的最大尺寸限制（宽/高均不能超过此值），避免因内存过大导致崩溃。
        /// Chromium 内部对截图尺寸有限制，通常建议不超过 8192。
        /// </summary>
        private const int MAX_SINGLE_CAPTURE_DIM = 8192;

        /// <summary>
        /// WebView2 的核心对象，用于执行脚本、调用 DevTools 协议等。
        /// </summary>
        private readonly CoreWebView2 _coreWebView2;

        /// <summary>
        /// 用于支持异步操作取消的令牌源。
        /// </summary>
        private CancellationTokenSource? _cts;

        /// <summary>
        /// 截图保存的目标文件夹路径。
        /// </summary>
        private string _saveFolder;

        /// <summary>
        /// 获取或设置截图保存目录。设置时会自动创建该目录（若不存在）。
        /// </summary>
        public string SaveFolder
        {
            get => _saveFolder;
            set
            {
                if (string.IsNullOrEmpty(value))
                    throw new ArgumentNullException(nameof(value));
                _saveFolder = value;
                EnsureDirectoryExists(); // 确保目录存在
            }
        }

        /// <summary>
        /// 构造函数。
        /// </summary>
        /// <param name="coreWebView2">已初始化的 CoreWebView2 实例。</param>
        /// <param name="defaultSaveFolder">默认保存路径，默认为 C:\Temp。</param>
        public WebCaptureHelper(CoreWebView2 coreWebView2, string defaultSaveFolder = @"C:\Temp")
        {
            _coreWebView2 = coreWebView2 ?? throw new ArgumentNullException(nameof(coreWebView2));
            _saveFolder = defaultSaveFolder;
            EnsureDirectoryExists(); // 初始化时确保目录存在
        }

        // ===== 公共方法 =====

        /// <summary>
        /// 导航到指定 URL，并自动标准化（如补全协议、添加 www 等）。
        /// </summary>
        /// <param name="url">目标网址，可为不完整形式（如 "baidu.com"）。</param>
        /// <returns>导航完成后的任务。</returns>
        public async Task NavigateAsync(string url)
        {
            if (string.IsNullOrWhiteSpace(url))
                throw new ArgumentException("URL 不能为空。");

            url = NormalizeUrl(url); // 标准化 URL

            var tcs = new TaskCompletionSource<bool>();

            // 监听导航完成事件
            void handler(object sender, CoreWebView2NavigationCompletedEventArgs e)
            {
                _coreWebView2.NavigationCompleted -= handler; // 避免重复触发
                tcs.SetResult(true);
            }

            _coreWebView2.NavigationCompleted += handler;
            _coreWebView2.Navigate(url);
            await tcs.Task;
        }

        /// <summary>
        /// 执行全页截图，支持取消操作。
        /// </summary>
        /// <param name="cancellationToken">外部传入的取消令牌（可选）。</param>
        /// <returns>截图保存的完整文件路径。</returns>
        /// <exception cref="OperationCanceledException">用户取消操作时抛出。</exception>
        public async Task<string> CaptureFullPageAsync(CancellationToken cancellationToken = default)
        {
            // 取消之前的截图任务（如有）
            _cts?.Cancel();
            _cts?.Dispose();

            // 创建新的取消令牌源，支持链式取消
            _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
            OnCaptureStart(); // 通知 UI 开始
            Log("开始截取整个网页...");

            try
            {
                // 执行实际截图逻辑
                string result = await CaptureFullPageRobustAsync(_saveFolder, _cts.Token);
                OnCaptureComplete(result); // 通知完成
                return result;
            }
            catch (OperationCanceledException)
            {
                OnCaptureCancelled(); // 通知取消
                Log("用户已取消截图。");
                throw;
            }
            finally
            {
                _cts?.Dispose();
                _cts = null;
            }
        }

        /// <summary>
        /// 立即取消当前正在进行的截图任务。
        /// </summary>
        public void CancelCapture()
        {
            _cts?.Cancel();
        }

        // ===== 私有辅助方法 =====

        /// <summary>
        /// 向 OnLog 事件发送带时间戳的日志消息。
        /// </summary>
        /// <param name="message">日志内容。</param>
        private void Log(string message)
        {
            OnLog($"{DateTime.Now:HH:mm:ss} - {message}");
        }

        /// <summary>
        /// 向 OnProgress 事件报告进度，并可选附加状态文本。
        /// </summary>
        /// <param name="percent">进度百分比（0-100）。</param>
        /// <param name="status">状态描述（如“正在拼接图片”）。</param>
        private void ReportProgress(int percent, string status = "")
        {
            percent = Math.Max(0, Math.Min(100, percent)); // 限制范围
            OnProgress(percent, status);
            if (!string.IsNullOrEmpty(status))
                Log(status);
        }

        /// <summary>
        /// 确保保存目录存在，若不存在则自动创建。
        /// </summary>
        private void EnsureDirectoryExists()
        {
            if (!Directory.Exists(_saveFolder))
            {
                Directory.CreateDirectory(_saveFolder);
                Log($"自动创建保存文件夹: {_saveFolder}");
            }
        }

        /// <summary>
        /// 标准化输入的 URL：
        /// - 补全 http/https 协议（默认 https）
        /// - 自动在域名前加 www（仅当原域名不含 www 时）
        /// </summary>
        /// <param name="input">原始 URL 字符串。</param>
        /// <returns>标准化后的完整 URL。</returns>
        private string NormalizeUrl(string input)
        {
            if (string.IsNullOrWhiteSpace(input))
                return string.Empty;

            input = input.Trim();

            // 补全协议
            if (!input.StartsWith("http://", StringComparison.OrdinalIgnoreCase) &&
                !input.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
            {
                input = "https://" + input;
            }

            try
            {
                Uri uri = new Uri(input);
                string host = uri.Host;

                // 自动添加 www（如果缺失）
                if (!host.StartsWith("www.", StringComparison.OrdinalIgnoreCase))
                {
                    host = "www." + host;
                    UriBuilder builder = new UriBuilder(uri) { Host = host };
                    input = builder.Uri.ToString();
                }
            }
            catch
            {
                // 若解析失败，保留原始输入（避免崩溃）
            }

            return input;
        }

        /// <summary>
        /// 处理 ExecuteScriptAsync 返回的原始 JSON 字符串。
        /// WebView2 返回的字符串常被双引号包裹并转义，需解包还原。
        /// </summary>
        /// <param name="raw">WebView2 返回的原始字符串。</param>
        /// <returns>解包后的有效 JSON 字符串。</returns>
        private static string UnwrapExecuteScriptResult(string raw)
        {
            if (string.IsNullOrEmpty(raw)) return "{}";
            // 如果是被双引号包裹的字符串（如 "\"{...}\""），则去除外层引号并反转义
            if (raw.Length >= 2 && raw[0] == '"' && raw[^1] == '"')
            {
                var inner = raw.Substring(1, raw.Length - 2);
                inner = inner.Replace("\\\"", "\"").Replace("\\\\", "\\");
                return inner;
            }
            return raw;
        }

        /// <summary>
        /// 主截图逻辑：先尝试单次截图，失败则回退到滚动拼接。
        /// </summary>
        /// <param name="saveFolder">保存目录。</param>
        /// <param name="token">取消令牌。</param>
        /// <returns>截图文件路径。</returns>
        private async Task<string> CaptureFullPageRobustAsync(string saveFolder, CancellationToken token)
        {
            var core = _coreWebView2;

            // 启用 Page DevTools 域（必须）
            Log("启用 Page 模块...");
            await core.CallDevToolsProtocolMethodAsync("Page.enable", "{}");

            // 注入 JS：解除页面高度/溢出限制，确保内容完全展开
            var helperJs = @"
(function(){
    try {document.documentElement.style.overflow='visible';document.body.style.overflow='visible';}catch(e){}
    try {document.documentElement.style.height='auto';document.body.style.height='auto';}catch(e){}
    try{
        var els=document.querySelectorAll('*');
        for(var i=0;i<els.length;i++){
            var s=getComputedStyle(els[i]);
            if(s&&(s.overflow==='hidden'||s.overflowY==='hidden'||s.overflowX==='hidden'))
                els[i].style.overflow='visible';
            els[i].style.maxHeight='none';
            els[i].style.minHeight='0';
        }
    }catch(e){}
    return true;
})();";
            await core.ExecuteScriptAsync(helperJs);
            Log("网页已展开。");

            // 特殊处理：修正轮播图宽度（防止因 overflow:hidden 导致内容被裁剪）
            var fixCarouselJs = @"
(function(){
    try{
        var carousels = document.querySelectorAll('.carousel, .slider, .slick-slider');
        carousels.forEach(function(c){
            c.style.width = '100%';
            c.style.maxWidth = '100%';
            c.style.overflow = 'hidden';
        });
    }catch(e){}
})();";
            await core.ExecuteScriptAsync(fixCarouselJs);
            Log("轮播图宽度已修正。");

            await Task.Delay(300); // 给 DOM 更新留出时间

            // 获取页面真实尺寸和设备像素比（DPR）
            var raw = await core.ExecuteScriptAsync(
                "JSON.stringify({ width: Math.max(document.documentElement.scrollWidth||0, document.body.scrollWidth||0)," +
                "height: Math.max(document.documentElement.scrollHeight||0, document.body.scrollHeight||0)," +
                "viewportWidth: window.innerWidth, viewportHeight: window.innerHeight, dpr: window.devicePixelRatio||1 })");

            var json = UnwrapExecuteScriptResult(raw);
            using var doc = JsonDocument.Parse(json);
            int contentWidth = doc.RootElement.GetProperty("width").GetInt32();
            int contentHeight = doc.RootElement.GetProperty("height").GetInt32();
            int viewportWidth = doc.RootElement.GetProperty("viewportWidth").GetInt32();
            int viewportHeight = doc.RootElement.GetProperty("viewportHeight").GetInt32();
            double dpr = doc.RootElement.GetProperty("dpr").GetDouble();

            Log($"网页内容尺寸: {contentWidth}x{contentHeight}, 视口: {viewportWidth}x{viewportHeight}, DPR={dpr}");

            // 限制最大宽度（避免某些网站因轮播图无限拉宽）
            const int maxWidthPx = 1920;
            contentWidth = Math.Min(contentWidth, maxWidthPx);
            contentWidth = Math.Max(1, contentWidth);
            contentHeight = Math.Max(1, contentHeight);

            // 尝试单次截图（高效且高质量）
            if (contentWidth <= MAX_SINGLE_CAPTURE_DIM && contentHeight <= MAX_SINGLE_CAPTURE_DIM)
            {
                try
                {
                    Log("尝试单次截图...");
                    ReportProgress(5, "单次截图");

                    // 设置虚拟设备尺寸（模拟大屏）
                    var metrics = new { width = contentWidth, height = contentHeight, deviceScaleFactor = dpr, mobile = false, scale = 1 };
                    await core.CallDevToolsProtocolMethodAsync("Emulation.setDeviceMetricsOverride", JsonSerializer.Serialize(metrics));

                    // 执行截图
                    var captureParams = new { format = "png", fromSurface = true };
                    var captureJson = await core.CallDevToolsProtocolMethodAsync("Page.captureScreenshot", JsonSerializer.Serialize(captureParams));
                    using var jr = JsonDocument.Parse(captureJson);
                    var base64 = jr.RootElement.GetProperty("data").GetString();
                    if (string.IsNullOrEmpty(base64)) throw new Exception("返回截图为空。");

                    // 保存文件
                    var bytes = Convert.FromBase64String(base64);
                    string filename = $"网页截图_{DateTime.Now:yyyyMMdd_HHmmss}.png";
                    string path = Path.Combine(saveFolder, filename);
                    await File.WriteAllBytesAsync(path, bytes);

                    Log("单次截图成功。");
                    await core.CallDevToolsProtocolMethodAsync("Emulation.clearDeviceMetricsOverride", "{}"); // 恢复原状
                    ReportProgress(100, "已保存 (单次截图)");
                    return path;
                }
                catch (Exception ex)
                {
                    Log($"单次截图失败: {ex.Message}，回退到滚动拼接。");
                    await core.CallDevToolsProtocolMethodAsync("Emulation.clearDeviceMetricsOverride", "{}");
                }
            }

            Log("内容过大或单次截图失败，使用滚动拼接截图。");
            return await CaptureByScrollAsync(core, contentWidth, contentHeight, viewportWidth, viewportHeight, dpr, saveFolder, token);
        }

        /// <summary>
        /// 通过滚动视口分段截图并拼接成完整图片。
        /// </summary>
        private async Task<string> CaptureByScrollAsync(
            CoreWebView2 core,
            int contentWidth,
            int contentHeight,
            int viewportWidth,
            int viewportHeight,
            double dpr,
            string saveFolder,
            CancellationToken token)
        {
            int vertSeg = (int)Math.Ceiling((double)contentHeight / viewportHeight);
            Log($"滚动拼接: {vertSeg} 段");
            ReportProgress(1, "开始滚动拼接");

            var images = new List<Image>();
            try
            {
                // 分段滚动并截图
                for (int i = 0; i < vertSeg; i++)
                {
                    token.ThrowIfCancellationRequested();

                    int scrollY = i * viewportHeight;
                    AppendLog($"滚动到 y={scrollY} (段 {i + 1}/{vertSeg})");
                    ReportProgress((int)((i / (double)vertSeg) * 70) + 1, $"滚动 {i + 1}/{vertSeg}");

                    // 平滑滚动 JS（先滚到略上方再精准定位，减少动画干扰）
                    var jsScroll = $@"
                    (async function(){{
                      window.scrollTo(0, Math.max(0, {scrollY}-50)); 
                      await new Promise(r=>setTimeout(r,120));
                      window.scrollTo(0,{scrollY});
                      await new Promise(r=>setTimeout(r,350));
                      return true;
                    }})();";
                    await core.ExecuteScriptAsync(jsScroll);

                    // 截图当前视口
                    var captureJson = await core.CallDevToolsProtocolMethodAsync("Page.captureScreenshot", "{}");
                    using var jr = JsonDocument.Parse(captureJson);
                    var base64 = jr.RootElement.GetProperty("data").GetString();
                    if (string.IsNullOrEmpty(base64)) throw new Exception($"段 {i + 1} 截图为空");

                    var bytes = Convert.FromBase64String(base64);
                    using var ms = new MemoryStream(bytes);
                    var img = Image.FromStream(ms);
                    images.Add(new Bitmap(img)); // 转为可编辑位图

                    await Task.Delay(120); // 防止过快
                }

                token.ThrowIfCancellationRequested();

                // 拼接所有片段
                Log("开始拼接图片...");
                ReportProgress(75, "拼接中");

                int finalWidthPx = (int)Math.Round(contentWidth * dpr);
                int finalHeightPx = (int)Math.Round(contentHeight * dpr);

                var finalBitmap = new Bitmap(finalWidthPx, finalHeightPx);
                using (var g = Graphics.FromImage(finalBitmap))
                {
                    g.Clear(Color.White); // 背景设为白色
                    int currentYPx = 0;
                    for (int i = 0; i < images.Count; i++)
                    {
                        var seg = images[i];
                        g.DrawImage(seg, 0, currentYPx, seg.Width, seg.Height);
                        currentYPx += seg.Height;
                        seg.Dispose();

                        var p = 75 + (int)(((i + 1) / (double)images.Count) * 20);
                        ReportProgress(p, $"拼接 {i + 1}/{images.Count}");
                    }
                }

                // 保存最终图片
                string filename = _cts?.IsCancellationRequested == true
                    ? $"网页截图_未完成_{DateTime.Now:yyyyMMdd_HHmmss}.png"
                    : $"网页截图_拼接_{DateTime.Now:yyyyMMdd_HHmmss}.png";

                string path = Path.Combine(saveFolder, filename);
                finalBitmap.Save(path, ImageFormat.Png);
                finalBitmap.Dispose();

                Log("拼接完成，恢复滚动到顶部。");
                try { await core.ExecuteScriptAsync("window.scrollTo(0,0);"); } catch { }
                ReportProgress(100, $"已保存 ({(_cts?.IsCancellationRequested == true ? "未完成" : "完成")})");
                return path;
            }
            catch (OperationCanceledException)
            {
                // 用户取消时，保存已截取的部分
                if (images.Count > 0)
                {
                    Log("用户停止，保存已截取部分...");
                    string path = Path.Combine(saveFolder, $"网页截图_未完成_{DateTime.Now:yyyyMMdd_HHmmss}.png");
                    int finalWidthPx = (int)Math.Round(contentWidth * dpr);
                    int finalHeightPx = (int)Math.Round(contentHeight * dpr);

                    var finalBitmap = new Bitmap(finalWidthPx, finalHeightPx);
                    using (var g = Graphics.FromImage(finalBitmap))
                    {
                        g.Clear(Color.White);
                        int currentYPx = 0;
                        foreach (var seg in images)
                        {
                            g.DrawImage(seg, 0, currentYPx, seg.Width, seg.Height);
                            currentYPx += seg.Height;
                            seg.Dispose();
                        }
                    }
                    finalBitmap.Save(path, ImageFormat.Png);
                    finalBitmap.Dispose();
                    return path;
                }
                throw;
            }
            finally
            {
                // 清理资源
                foreach (var img in images)
                    img?.Dispose();
            }
        }

        // 兼容旧命名（可删除）
        private void AppendLog(string text) => Log(text);

        /// <summary>
        /// 释放资源。
        /// </summary>
        public void Dispose()
        {
            _cts?.Dispose();
        }
    }
}
